﻿//#include rfm

(function() {

var _isSupported = false,
	_isPlayBook = navigator.userAgent.indexOf('PlayBook') > -1,
	_isAndroid = !_isPlayBook && navigator.userAgent.indexOf('Android') > -1,
	_clickAreaWidth = _isPlayBook ? 4 : 1,
	_clickAreaHeight = _isPlayBook ? 4 : 1,
	_generatedEvents = {},
    TOUCHSTART_EVENT = 'mousedown',
    TOUCHMOVE_EVENT = 'mousemove',
    TOUCHEND_EVENT = 'mouseup',
    UNDEFINED;

(function() {
    var div = document.createElement('div');
    div.setAttribute('ontouchmove', 'return');
    if (div.ontouchmove instanceof Function) {
        _isSupported = true;
        TOUCHSTART_EVENT = 'touchstart';
        TOUCHMOVE_EVENT = 'touchmove';
        TOUCHEND_EVENT = 'touchend';
    }
})();

function _clonePoint(point) {
    return {
        screenX: point.screenX,
        screenY: point.screenY,
        clientX: point.clientX,
        clientY: point.clientY,
        pageX: point.pageX,
        pageY: point.pageY
    };
}

function _contains(touchList, identifier) {
    for (var i = touchList.length - 1; i > -1; i--) {
        if (touchList[i].identifier === identifier) {
            return true;
        }
    }
    return false;
}

function _coordsEquals(point1, point2) {
    return point1
        && point2
        && point1.screenX === point2.screenX
        && point1.screenY === point2.screenY;
}

function _calculateAngle(point1, point2) {
    return Math.atan((point2.screenX - point1.screenX) / (point2.screenY - point1.screenY)) * 180 / Math.PI;
}

function _calculateDistance(point1, point2) {
    return Math.sqrt((point2.screenX - point1.screenX) * (point2.screenX - point1.screen) + (point2.screenY - point1.screenY) * (point2.screenY - point1.screenY));
}


/**
 * Touch utility object
 */
rfm.Touch = {
    /**
     * {String} (Constant) Name of the touch start event (mousedown on non-touch devices)
     */
    START_EVENT: TOUCHSTART_EVENT,
    /**
     * {String} (Constant) Name of the touch move event (mousemove on non-touch devices)
     */
    MOVE_EVENT: TOUCHMOVE_EVENT,
    /**
     * {String} (Constant) Name of the touch end event (mouseup on non-touch devices)
     */
    END_EVENT: TOUCHEND_EVENT,
    /**
     * Whether or not touch events are supported
     * @returns {Boolean}
     */
    isSupported: function() {
        return _isSupported;
    },
    /**
     * Gets the target coordinates from a mouse or touch event
     * @param {Event} e The event
     * @param {Number} identifier (Optional) The unique identifier of the touch
     * @returns {Object} An object containing the screen, client, and page coordinates of the event
     */
    getCoords: function(e, identifier) {
        var point;
        if (e.touches) {
            if (identifier) {
                for (var i = e.touches.length - 1; i > -1; i--) {
                    if (e.touches[i].identifier == identifier) {
                        point = e.touches[i];
                        break;
                    }
                }
            }
            else {
                point = e.touches[0];
            }
        }
        else {
            point = e;
        }
        return {
            screenX: point.screenX,
            screenY: point.screenY,
            clientX: point.clientX,
            clientY: point.clientY,
            pageX: point.pageX,
            pageY: point.pageY
        };
    }
};

/**
 * Binds a callback to the touchstart event (when supported) or the mousedown event (when not supported)
 * @param {Function} callback The event handler
 * @param {Boolean} capture (Optional) Whether or not to use the capturing phase (defaults to false)
 * @returns {Element} The element
 */
Element.prototype.$touchstart = function(callback, capture) {
	this.addEventListener(_isSupported ? 'touchstart' : 'mousedown', callback, capture === true);
	return this;
};

/**
 * Unbinds a callback from the touchstart event (when supported) or the mousedown event (when not supported)
 * @param {Function} callback The event handler
 * @param {Boolean} capture (Optional) Whether or not to use the capturing phase (defaults to false)
 * @returns {Element} The element
 */
Element.prototype.$unbind_touchstart = function(callback, capture) {
	this.removeEventListener(_isSupported ? 'touchstart' : 'mousedown', callback, capture === true);
	return this;
};

/**
 * Binds a callback to the touchmove event (when supported) or the mousemove event (when not supported)
 * @param {Function} callback The event handler
 * @param {Boolean} capture (Optional) Whether or not to use the capturing phase (defaults to false)
 * @returns {Element} The element
 */
Element.prototype.$touchmove = function(callback, capture) {
	this.addEventListener(_isSupported ? 'touchmove' : 'mousemove', callback, capture === true);
	return this;
};

/**
 * Unbinds a callback from the touchmove event (when supported) or the mousemove event (when not supported)
 * @param {Function} callback The event handler
 * @param {Boolean} capture (Optional) Whether or not to use the capturing phase (defaults to false)
 * @returns {Element} The element
 */
Element.prototype.$unbind_touchmove = function(callback, capture) {
	this.removeEventListener(_isSupported ? 'touchmove' : 'mousemove', callback, capture === true);
	return this;
};

/**
 * Binds a callback to the touchend event (when supported) or the mouseup event (when not supported)
 * @param {Function} callback The event handler
 * @param {Boolean} capture (Optional) Whether or not to use the capturing phase (defaults to false)
 * @returns {Element} The element
 */
Element.prototype.$touchend = function(callback, capture) {
	this.addEventListener(_isSupported ? 'touchend' : 'mouseup', callback, capture === true);
	return this;
};

/**
 * Unbinds a callback to the touchend event (when supported) or the mouseup event (when not supported)
 * @param {Function} callback The event handler
 * @param {Boolean} capture (Optional) Whether or not to use the capturing phase (defaults to false)
 * @returns {Element} The element
 */
Element.prototype.$unbind_touchend = function(callback, capture) {
	this.removeEventListener(_isSupported ? 'touchend' : 'mouseup', callback, capture === true);
	return this;
};

/**
 * Binds handlers to multitouch events of an element, and supplies useful metrics to those handlers. Note that there are no mouse equivalents for gestures
 * @param {Function} onstart The callback to execute when the user begins touching the element with two fingers
 * @param {Function} onchange The callback to execute when the user one or more of the fingers touching the element
 * @param {Function} onend The callback to execute when one or more of the gesturing fingers ends contact with the screen
 * @returns {Element} The element
 */
Element.prototype.$gesture = function(onstart, onchange, onend) {
    var holdAttributeName = 'data-rfm-gesture-' + rfm.guid(),
        identifier1 = UNDEFINED,
        identifier2 = UNDEFINED,
        originPoint1 = null,
        originPoint2 = null,
        lastPoint1 = null,
        lastPoint2 = null,
        lastDeltaX1 = 0,
        lastDeltaX2 = 0,
        lastDeltaY1 = 0,
        lastDeltaY2 = 0,
        lastRotation = 0,
        lastScale = 1,
        lastRotationDelta = 0,
        lastScaleDelta = 0,
        lastDistance = 0,
        performedMove1 = false,
        performedMove2 = false;
    function resetValues() {
        originPoint1 = originPoint2 = lastPoint1 = lastPoint2 = null;
        lastDeltaX1 = lastDeltaX2 = lastDeltaY1 = lastDeltaY2 = lastRotation = lastDistance = lastRotationDelta = lastScaleDelta = 0;
        lastScale = 1;
        performedMove1 = performedMove2 = false;
    }
    function getEventData(currentPoint1, currentPoint2) {
        return {
            touch1: {
                identifier: identifier1,
                currentPoint: _clonePoint(currentPoint1),
                originPoint:  _clonePoint(originPoint1),
                lastPoint:  _clonePoint(lastPoint1),
                deltaX: lastDeltaX1,
                deltaY: lastDeltaY1,
                performedMove: performedMove1
            },
            touch2: {
                identifier: identifier2,
                currentPoint:  _clonePoint(currentPoint2),
                originPoint:  _clonePoint(originPoint2),
                lastPoint:  _clonePoint(lastPoint2),
                deltaX: lastDeltaX2,
                deltaY: lastDeltaY2,
                performedMove: performedMove2
            },
            rotationDelta: lastRotationDelta,
            scaleDelta: lastScaleDelta,
            rotation: lastRotation,
            scale: lastScale,
            distance: lastDistance
        }
    }
    function isOwnEvent(e) {
        return _contains(e.changedTouches, identifier1) || _contains(e.changedTouches, identifier2);
    }
    this.addEventListener('touchstart', function(e) {
    	switch (e.target.nodeName) {
	    	case 'SELECT':
	    	case 'INPUT':
	    		return;
    	}
        var target = this;
        if (this.getAttribute(holdAttributeName)) {
            return;
        }
        if (!identifier1) {
            identifier1 = e.changedTouches[0].identifier;
        }
        if (!identifier2) {
            if (_contains(e.touches, identifier1)) {
                for (var i = e.changedTouches.length - 1; i > -1; i--) {
                    if (e.changedTouches[i].identifier != identifier1) {
                        identifier2 = e.changedTouches[i].identifier;
                        break;
                    }
                }
            }
            else {
                identifier1 = e.changedTouches[0].identifier;
            }
        }
        if (identifier1 && identifier2 && (isOwnEvent(e.changedTouches, identifier1) || isOwnEvent(e.changedTouches, identifier2))) {
            function ontouchmove_body(e) {
                if (target && isOwnEvent(e)) {
                    var currentPoint1 = rfm.Touch.getCoords(e, identifier1),
                        currentPoint2 = rfm.Touch.getCoords(e, identifier2),
                        currentDistance = _calculateDistance(currentPoint1, currentPoint2),
                        currentRotation = _calculateAngle(currentPoint1, currentPoint2),
                        currentScale = currentDistance / lastDistance,
                        eventData;
                    performedMove1 = performedMove1 || _coordsEquals(currentPoint1, lastPoint1);
                    performedMove2 = performedMove2 || _coordsEquals(currentPoint2, lastPoint2);
                    lastDeltaX1 = currentPoint1.screenX - lastPoint1.screenX;
                    lastDeltaX2 = currentPoint2.screenX - lastPoint2.screenX;
                    lastDeltaY1 = currentPoint1.screenY - lastPoint1.screenY;
                    lastDeltaY2 = currentPoint2.screenY - lastPoint2.screenY;
                    lastRotationDelta = currentRotation - lastRotation;
                    lastScaleDelta = currentScale - lastScale;
                    lastScale = currentScale;
                    lastDistance = currentDistance;
                    lastRotation = currentRotation;
                    eventData = getEventData(currentPoint1, currentPoint2);
                    if (onchange) {
                        onchange.call(target, e, eventData);
                    }
                    lastPoint1 = currentPoint1;
                    lastPoint2 = currentPoint2;
                }
            }
            function ontouchend_body(e) {
                if (target && isOwnEvent(e)) {
                    var eventData = getEventData(lastPoint1, lastPoint2);
                    if (onend) {
                        onend.call(target, e, eventData);
                    }
                    target.removeAttribute(holdAttributeName);
                    target = null;
                    identifier1 = identifier2 = UNDEFINED;
                    resetValues();
                    document.body.removeEventListener('touchmove', ontouchmove_body, false);
                    document.body.removeEventListener('touchend', ontouchend_body, false);
                }
            }
            document.body.addEventListener('touchmove', ontouchmove_body, false);
            document.body.addEventListener('touchend', ontouchend_body, false);
            resetValues();
            originPoint1 = lastPoint1 = rfm.Touch.getCoords(e, identifier1);
            originPoint2 = lastPoint2 = rfm.Touch.getCoords(e, identifier2);
            lastDistance = _calculateDistance(originPoint1, originPoint2);
            lastRotation = _calculateAngle(originPoint1, originPoint2);
            if (onstart) {
                onstart.call(this, e, getEventData(originPoint1, originPoint2));
            }
        }
    }, false);
    return this;
};

/**
 * Binds handlers to touch start, move, and end events of an element, and supplies useful metrics to those handlers
 * @param {Function} onstart The callback to execute when the user begins touching the element (mousedown on desktop)
 * @param {Function} onmove The callback to execute when the user moves the pointer after touching the element (mousemove on desktop)
 * @param {Function} onend The callback to execute when the user ends contact with the screen (mouseup on desktop)
 * @param {Boolean} allowClick (Optional) Whether or not the default click action should be performed when the pointer does not move (defaults to false)
 * @param {Boolean} allowMultitouch (Optional) Whether or not to only recognize one touch on the element at a time (defaults to false)
 * @returns {Element} The element
 */
Element.prototype.$drag = function(onstart, onmove, onend, allowClick, allowMultitouch) {
    var holdAttributeName = 'data-rfm-touch-' + rfm.guid();
    this.addEventListener(TOUCHSTART_EVENT, function(e) {
    	switch (e.target.nodeName) {
	    	case 'SELECT':
	    	case 'INPUT':
	    		return;
    	}
        if (!allowMultitouch) {
            if (this.getAttribute(holdAttributeName)) {
                return;
            }
            else {
                this.setAttribute(holdAttributeName, '1');
            }
        }
        e.preventDefault();
        var originPoint = null,
            lastPoint = null,
            lastDeltaX = 0,
            lastDeltaY = 0,
            performedMove = false,
            target = this,
            startTarget = e.target,
            identifier = UNDEFINED;
        if (_isSupported && e.changedTouches) {
            identifier = e.changedTouches[0].identifier;
        }
        function getEventData(currentPoint) {
            return {
                identifier: identifier,
                currentPoint: _clonePoint(currentPoint),
                originPoint: _clonePoint(originPoint),
                lastPoint: _clonePoint(lastPoint),
                deltaX: lastDeltaX,
                deltaY: lastDeltaY,
                performedMove: performedMove
            };
        }
        function isOwnEvent(e) {
            if (identifier) {
                for (var i = e.changedTouches.length - 1; i > -1; i--) {
                    if (e.changedTouches[i].identifier == identifier) {
                        return true;
                    }
                }
                return false;
            }
            else {
                return true;
            }
        }
        function ontouchmove_body(e) {
            if (target && isOwnEvent(e)) {
                e.preventDefault();
                performedMove = true;
                var currentPoint = rfm.Touch.getCoords(e, identifier),
                	deltaX = currentPoint.screenX - lastPoint.screenX,
                	deltaY = currentPoint.screenY - lastPoint.screenY,
                    eventData;
                if (Math.abs(deltaX) > _clickAreaWidth || Math.abs(deltaY) > _clickAreaWidth) {
	                lastDeltaX = deltaX;
	                lastDeltaY = deltaY;
	                eventData = getEventData(currentPoint);
	                if (onmove) {
	                    onmove.call(target, e, eventData);
	                }
	                lastPoint = currentPoint;
                }
            }
        }
        function ontouchend_body(e) {
            if (target && isOwnEvent(e)) {
                e.preventDefault();
                var eventData = getEventData(lastPoint);
                if (onend) {
                    onend.call(target, e, eventData);
                }
                if (_isSupported && allowClick && !_generatedEvents[identifier] && Math.abs(originPoint.screenX - lastPoint.screenX) < _clickAreaWidth && Math.abs(originPoint.screenY - lastPoint.screenY) < _clickAreaHeight) {
	                var evt = document.createEvent('MouseEvents');
	                evt.initMouseEvent('click', true, true, window, 0, originPoint.screenX, originPoint.screenY, originPoint.clientX, originPoint.clientY, false, false, false, false, 0, null);
	                startTarget.dispatchEvent(evt);
	                _generatedEvents[identifier] = true;
	                setTimeout(function() {
	                	delete _generatedEvents[identifier];
	                }, 0);
                }
                target.removeAttribute(holdAttributeName);
                startTarget = target = originPoint = lastPoint = null;
                lastDeltaX = lastDeltaY = 0;
                performedMove = false;
                document.body.removeEventListener(TOUCHMOVE_EVENT, ontouchmove_body, false);
                document.body.removeEventListener(TOUCHEND_EVENT, ontouchend_body, false);
            }
        }
        document.body.addEventListener(TOUCHMOVE_EVENT, ontouchmove_body, false);
        document.body.addEventListener(TOUCHEND_EVENT, ontouchend_body, false);
        originPoint = lastPoint = rfm.Touch.getCoords(e, identifier);
        if (onstart) {
            onstart.call(this, e, getEventData(originPoint));
        }
    }, false);
    return this;
};

})();